//! Utility types for pretending to be parts of world state, since we have to
//! flatten it all out in the bridge type.  Also provides utility types.

use std::collections::HashMap;

use craftdig_datamodel::entity::EntityId;
use godot::classes::{CharacterBody3D, Label, Material, PhysicsRayQueryParameters3D};
use godot::prelude::*;

use craftdig_math::*;

/// Screen elements.
pub struct HudView {
    lbl_coords: Gd<Label>,
    lbl_blkty: Gd<Label>,
}

impl HudView {
    pub fn new(lbl_coords: Gd<Label>, lbl_blkty: Gd<Label>) -> Self {
        Self {
            lbl_coords,
            lbl_blkty,
        }
    }

    pub fn set_player_coords_lbl(&mut self, player_wp: Vector3) {
        let m = format!(
            "X {:.5} / Y {:.5} / Z {:.5}",
            player_wp.x, player_wp.y, player_wp.z
        );
        self.lbl_coords.set_text(m.into());
    }

    pub fn set_block_lbl(&mut self, m: &str) {
        self.lbl_blkty.set_text(m.into());
    }
}

/// Player node, camera, selection, etc.
pub struct PlayerView {
    player: Gd<CharacterBody3D>,
    camera: Gd<Node3D>,
    selection: Gd<Node3D>,
}

impl PlayerView {
    pub fn new(player: Gd<CharacterBody3D>, camera: Gd<Node3D>, selection: Gd<Node3D>) -> Self {
        Self {
            player,
            camera,
            selection,
        }
    }

    /// Gets the player position.
    pub fn get_player_world_pos(&self) -> Vector3 {
        self.player.get_global_position()
    }

    /// Returns the player entity's global transform.
    ///
    /// This is used in calculating player movement.  We're doing a hack here by
    /// getting it from Godot instead of keeping track of this kind of
    /// information ourselves.  Eventually we'll do that, but today's not the
    /// day.
    pub fn get_player_global_transform(&mut self) -> Transform3D {
        self.player.get_global_transform()
    }

    /// Returns the chunk that the player is currently in.
    pub fn get_player_chunk(&self) -> ChunkCoord {
        let pp = self.get_player_world_pos();
        let pbp = BlkCoord::from(Vec3i::new(pp.x as i32, pp.y as i32, pp.z as i32));
        let (cc, _) = pbp.split();
        cc
    }

    /// Gets the position that the player sees from, typically the camera position (when first-person).
    pub fn get_player_eye_pos(&self) -> Vector3 {
        self.camera.get_global_position()
    }

    /// Gets the vector along the player's facing direction.
    pub fn get_player_facing(&self) -> Vector3 {
        let rot = Vector3 {
            x: self.camera.get_rotation().x,
            y: self.player.get_rotation().y,
            z: 0.0,
        };
        let basis = Basis::from_euler(EulerOrder::YXZ, rot);
        basis * Vector3::FORWARD
    }

    /// Uses the player's position and camera position to do a raycast for the player's target.
    pub fn get_player_target(&self, reach_length: f32) -> Option<(Vector3, Vector3)> {
        // Do some math to figure out where the player is facing.
        let pos = self.get_player_eye_pos();
        let facing = self.get_player_facing();
        let target_limit = pos + facing * reach_length;

        // Do the raycast.
        let mut ray_query = PhysicsRayQueryParameters3D::create(pos, target_limit)
            .expect("bridge: create ray query");
        let mut excludes = Array::new();
        let collider_rid = self.player.get_rid();
        excludes.push(collider_rid);
        ray_query.set_exclude(&excludes);
        let mut world = self
            .player
            .get_world_3d()
            .expect("bridge: player missing world");
        let mut space_state = world
            .get_direct_space_state()
            .expect("bridge: get space state");
        let raycast_result = space_state.intersect_ray(ray_query);

        // If the result dictionary has something it means we hit something.
        if !raycast_result.is_empty() {
            let pos = raycast_result.get("position").expect("bridge: get ray pos");
            let pos = pos.try_to::<Vector3>().expect("bridge: get ray pos");
            let norm = raycast_result.get("normal").expect("bridge: get ray norm");
            let norm = norm.try_to::<Vector3>().expect("bridge: get ray pos");
            Some((pos, norm))
        } else {
            None
        }
    }

    /// Sets the block selection.
    pub fn set_selection(&mut self, pos: Vector3, dims: Vector3) {
        self.selection.set_position(pos);
        self.selection.set_scale(dims);
        self.selection.set_visible(true);
    }

    /// Clears the block selection by making it invisble.
    pub fn clear_selection(&mut self) {
        self.selection.set_visible(false);
    }
}

/// Refs to nodes for the world view, like the chunks root.
pub struct WorldSceneView {
    chunks_root: Gd<Node3D>,

    /// Chunk nodes for each chunk.
    chunks: HashMap<ChunkCoord, Gd<Node3D>>,

    /// Entity nodes.
    entities: HashMap<EntityId, Gd<Node3D>>,
}

impl WorldSceneView {
    pub fn new(chunks_root: Gd<Node3D>) -> Self {
        Self {
            chunks_root,
            chunks: HashMap::new(),
            entities: HashMap::new(),
        }
    }

    /// Manually set the entity node without performing checks.
    ///
    /// This is mainly meant for inserting the player entity into the tracking
    /// so we can update it like any other.
    pub fn set_entity_node_manually(&mut self, eid: EntityId, node: Gd<Node3D>) {
        if self.entities.insert(eid, node).is_some() {
            godot_warn!("bridge: overwrite entity {eid:?} node in scene");
        }
    }

    /// Inserts a chunk, overwriting any existing node.
    ///
    /// This probably breaks if you pass an existing chunk, so don't do that.
    pub fn insert_chunk(&mut self, coord: ChunkCoord, mut new_chunk_cont: Gd<Node3D>) {
        self.try_remove_chunk(coord);

        // Move the new chunk to where it's supposed to be in the world.
        let chunk_gpos = Vector3::new(
            (coord.x() * CHUNK_SIZE_I32) as f32,
            (coord.y() * CHUNK_SIZE_I32) as f32,
            (coord.z() * CHUNK_SIZE_I32) as f32,
        );
        new_chunk_cont.set_position(chunk_gpos);

        self.chunks_root.add_child(new_chunk_cont.clone());
        self.chunks.insert(coord, new_chunk_cont);
    }

    /// Tries to remove a chunk from the view, if there is one.
    pub fn try_remove_chunk(&mut self, coord: ChunkCoord) -> bool {
        if let Some(mut node) = self.chunks.remove(&coord) {
            self.chunks_root.remove_child(&node);
            node.queue_free();
            true
        } else {
            false
        }
    }

    /// Sets the position of an entity.
    pub fn set_entity_position(&mut self, eid: &EntityId, pos: Vec3f) {
        let vec = Vector3::new(pos.x(), pos.y(), pos.z());
        if let Some(entity) = self.entities.get_mut(eid) {
            entity.set_position(vec);
        } else {
            godot_warn!("bridge: tried to set position of entity {eid:?} with no node");
        }
    }
}
